# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import logging
from typing import Union
from deprecated import deprecated
import functools
import time
from concurrent.futures import ThreadPoolExecutor
import math
from enum import Enum

import requests
from web3 import Web3, HTTPProvider
from web3.types import TxReceipt
from eth_typing.evm import Address
from eth_utils.address import is_checksum_address

from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.base.order_gateway import OrderSide, OrderType
from coin.exchange.uniswap_v3.kr_rest.product import UniswapProduct
from coin.exchange.uniswap_base.constants import (
    SYMBOL_TO_ADDRESS,
    TOKEN_TO_ADDRESS,
    TOKEN_TO_DECIMAL_MULTIPLIER,
    MAX_UINT_256,
    MAX_UINT_128,
    BP_TO_POOL_FEE_MULTIPLIER,
    ADDR_TO_DECIMAL_MULTIPLIER,
)
from coin.exchange.uniswap_v3.kr_rest.native_private_client_utils import (
    _load_contract,
    _addr_to_str,
    _nearest_tick,
    _parse_from_pool,
    _calculate_amount,
    run_once,
    TICK_SPACING,
)
from coin.exchange.uniswap_v3.kr_rest.uniswapNumber import UniswapNumber
from coin.exchange.uniswap_base.constants import (
    balance_checker_deployment_address,
)
from coin.exchange.uniswap_v3.kr_rest.constants import (
    deployment_address,
)


class UniswapOrderSide(Enum):
    BUY = "buy"
    SELL = "sell"


class UniswapOrderType(Enum):
    LIMIT = "limit"
    MARKET = "market"


class UniswapOrderStatus(Enum):
    LIVE = "live"
    FILLED = "filled"
    CANCELED = "cancelled"


def to_uniswap_order_side(order_side: OrderSide):
    assert isinstance(order_side, OrderSide), order_side
    return getattr(UniswapOrderSide, order_side.name)


def from_uniswap_order_side(order_side: UniswapOrderSide):
    assert isinstance(order_side, UniswapOrderSide), order_side
    return getattr(OrderSide, order_side.name)


def to_uniswap_order_type(order_type: OrderType):
    assert isinstance(order_type, OrderType), order_type
    return getattr(UniswapOrderSide, order_type.name)


def from_uniswap_order_type(order_type: UniswapOrderType):
    assert isinstance(order_type, UniswapOrderType), order_type
    return getattr(OrderType, order_type.name)


TEST_RPC_URL = "https://www.ankr.com/rpc/eth/"
TEST_WALLET_ADDRESS = "0x4Fb90EaA461074a927ABFDD73F64AFf2d30991be"

ETH_ADDRESS = "0x0000000000000000000000000000000000000000"
WETH9_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"


class UniswapV3NativePrivateClient(object):
    def __init__(
        self,
        wallet_address=None,
        uniswap_instance=None,
        # pool: Contract = None,
        # pool_info: Tuple[Address, Address, int] = None,
        pool_symbol_list=None,
        currency_symbol_list=None,
        acct=None,
        *args,
        **kwargs,
    ):
        assert pool_symbol_list is not None
        assert currency_symbol_list is not None
        assert is_checksum_address(wallet_address), wallet_address

        pool_symbol_list = list(set(pool_symbol_list))

        self._executor = ThreadPoolExecutor(max_workers=20)

        self.wallet_address = wallet_address or TEST_WALLET_ADDRESS
        self.acct = acct

        self.uniswap_instance = (
            Web3(HTTPProvider(TEST_RPC_URL))
            if (uniswap_instance is None)
            else uniswap_instance[0]
        )  # for passing a reference

        self.nft_tokens = []

        self.pools = {
            SYMBOL_TO_ADDRESS['ethereum'][pool_symbol]: _parse_from_pool(
                _load_contract(
                    self.uniswap_instance, "pool", SYMBOL_TO_ADDRESS['ethereum'][pool_symbol]
                )
            )
            for pool_symbol in pool_symbol_list
        }
        for pool_symbol in pool_symbol_list:
            self.pools[SYMBOL_TO_ADDRESS['ethereum'][pool_symbol]]["nft_tokens"] = []
            self.pools[SYMBOL_TO_ADDRESS['ethereum'][pool_symbol]]["pool_symbol"] = pool_symbol

        self.currencies = currency_symbol_list

        self.current_nonce = self.uniswap_instance.eth.get_transaction_count(
                        self.wallet_address
                    )

        self.update_nft_token_positions()
        # self.approve_NonfungiblePositionManager()

    def _quoter(self):
        return _load_contract(
            self.uniswap_instance,
            "quoter",
            deployment_address["Quoter"],
        )

    quoter = property(fget=_quoter, fset=None, fdel=None, doc=None)

    def _router(self):
        return _load_contract(
            self.uniswap_instance,
            "router",
            deployment_address["SwapRouter"],
        )

    router = property(fget=_router, fset=None, fdel=None, doc=None)

    def _nonFungiblePositionManager(self):
        return _load_contract(
            self.uniswap_instance,
            "nonFungiblePositionManager",
            deployment_address["NonfungiblePositionManager"],
        )

    nonFungiblePositionManager = property(
        fget=_nonFungiblePositionManager, fset=None, fdel=None, doc=None
    )

    def _addr_balance_checker(self):
        return _load_contract(
            self.uniswap_instance,
            "balanceChecker",
            balance_checker_deployment_address["mainnet"],
        )

    addr_balance_checker = property(
        fget=_addr_balance_checker, fset=None, fdel=None, doc=None
    )

    def _factory_contract(self):
        return _load_contract(
            self.uniswap_instance,
            "factory",
            deployment_address["UniswapV3Factory"],
        )

    factory_contract = property(fget=_factory_contract, fset=None, fdel=None, doc=None)

    def mint_liquidity_with_swap(
        self,
        pool_addr: Address,
        price_lower: int,
        price_upper: int,
        deadline: Union[None, int] = None,
        tolerance: float = 0.05,
        *args,
        **kwargs,
    ) -> TxReceipt:
        
        logging.info(f'Input|pool_addr={pool_addr}|price_lower={price_lower}|price_upper={price_upper}')
        
        if deadline is None:
            deadline = int(time.time()) + 2 * 60

        token_0, token_1, fee = (
            self.pools[str(pool_addr)]["token_base"],
            self.pools[str(pool_addr)]["token_quote"],
            self.pools[str(pool_addr)]["fee"],
        )

        pool_contract = _load_contract(self.uniswap_instance, "pool", pool_addr)

        _result = pool_contract.functions.slot0().call()
        token_0 = pool_contract.functions.token0().call()
        token_1 = pool_contract.functions.token1().call()

        curr_price = ((1.0 * _result[0] / (2 ** 96)) ** (2))  / ADDR_TO_DECIMAL_MULTIPLIER[token_0] * ADDR_TO_DECIMAL_MULTIPLIER[token_1]
        curr_tick = _result[1]

        tick_lower = curr_tick + math.log(price_lower / curr_price, 1.0001)
        tick_upper = curr_tick + math.log(price_upper / curr_price, 1.0001)

        tick_lower, tick_upper = _nearest_tick(tick_lower, fee), _nearest_tick(tick_upper, fee)

        logging.info(f'current_pool_price={curr_price}|current_tick={curr_tick}|tick_lower={tick_lower}|tick_upper={tick_upper}')
        assert tick_lower < tick_upper, "Invalid tick range"

        initial_amount_0 = UniswapNumber(token_0)
        initial_amount_1 = UniswapNumber(token_1)
        initial_amount_0.eth, initial_amount_1.eth = self.addr_balance_checker.functions.balances(
            [self.wallet_address],
            [token_0, token_1]
        ).call()

        initial_ratio: float = initial_amount_0.eth / initial_amount_1.eth
        
        _x, _y = _calculate_amount(tick_lower, tick_upper, curr_tick, 1e10)
        target_ratio: float = _x / _y
        
        logging.info(f'initial_ratio={initial_ratio}|target_ratio={target_ratio}')

        iter_limit: int = 10
        if math.isclose(initial_ratio, target_ratio, rel_tol=tolerance):
            pass
        elif initial_ratio > target_ratio:
            l = UniswapNumber(token_0, 0.0)
            r = UniswapNumber(token_0, float(initial_amount_0))
            # trade token_0 for token_1
            while (l < r and iter_limit > 0):
                iter_limit -= 1
                m = UniswapNumber(token_0, (float(l) + float(r)) / 2)
                input: float = m.eth
                output: float = self.quoter.functions.quoteExactInputSingle(
                    token_0, token_1, fee, m.eth, 0).call()  # not setting price limit
                
                new_ratio: float = \
                    (initial_amount_0.eth - input) / (initial_amount_1.eth + output)

                logging.info(f'range_left={float(l)}|range_right={float(r)}|input={input}|output={output}|new_ratio={new_ratio}')

                if (math.isclose(new_ratio, target_ratio, rel_tol=tolerance)):
                    l = m
                    break
                elif (new_ratio > target_ratio):
                    l = m
                else:
                    r = m
            logging.info(f'token_input={token_0}|token_output={token_1}|fee={fee}|amount={float(l)}')
            self.swap_in(token_0, token_1, fee, float(l))
        else:
            # trade token_1 for token_0
            l = UniswapNumber(token_1, 0.0)
            r = UniswapNumber(token_1, float(initial_amount_1))
            
            while (l < r and iter_limit > 0):
                iter_limit -= 1
                m = UniswapNumber(token_1, (float(l) + float(r)) / 2)
                input: float = m.eth
                output: float = self.quoter.functions.quoteExactInputSingle(
                    token_1, token_0, fee, m.eth, 0).call()  # not setting price limit

                new_ratio: float = \
                    (initial_amount_0.eth + output) / (initial_amount_1.eth - input)
                
                logging.info(f'range_left={float(l)}|range_right={float(r)}|input={input}|output={output}|new_ratio={new_ratio}')
                
                if (math.isclose(new_ratio, target_ratio, rel_tol=tolerance)):
                    l = m
                    break
                elif (new_ratio > target_ratio):
                    r = m
                else:
                    l = m
            logging.info(f'token_input={token_1}|token_output={token_0}|fee={fee}|amount={float(l)}')
            self.swap_in(token_1, token_0, fee, float(l))

        balance_0 = UniswapNumber(token_0)
        balance_1 = UniswapNumber(token_1)
        balance_0.eth, balance_1.eth = self.addr_balance_checker.functions.balances(
            [self.wallet_address],
            [token_0, token_1]
        ).call()
        
        logging.info(f'post_swap_balance|{float(balance_0)}|{float(balance_1)}')
        
        return self.mint_liquidity(pool_addr=pool_addr, amount_0=float(balance_0), amount_1=float(balance_1), price_lower=price_lower, price_upper=price_upper)

    def mint_liquidity(
        self,
        pool_addr,
        amount_0: float,
        amount_1: float,
        price_lower: int,
        price_upper: int,
        deadline: int = None,
        *args,
        **kwargs,
    ) -> TxReceipt:
        if deadline is None:
            deadline = int(time.time()) + 2 * 60

        token_0, token_1, fee = (
            self.pools[pool_addr]["token_base"],
            self.pools[pool_addr]["token_quote"],
            self.pools[pool_addr]["fee"],
        )

        pool_contract = _load_contract(self.uniswap_instance, "pool", pool_addr)
        _result = pool_contract.functions.slot0().call()
        curr_price = ((1.0 * _result[0] / (2 ** 96)) ** (-2)) * ADDR_TO_DECIMAL_MULTIPLIER[token_0] / ADDR_TO_DECIMAL_MULTIPLIER[token_1]
        curr_tick = _result[1]

        logging.info(f'current_pool_price={curr_price} | current_tick={curr_tick}')
        tick_lower = curr_tick + math.log(price_lower / curr_price, 1.0001)
        tick_upper = curr_tick + math.log(price_upper / curr_price, 1.0001)

        tick_lower, tick_upper = _nearest_tick(tick_lower, fee), _nearest_tick(
            tick_upper, fee
        )

        assert tick_lower < tick_upper, "Invalid tick range"

        nft_manager = self.nonFungiblePositionManager
        logging.info(f'MINT_LIQUIDITY : pool_addr={pool_addr}|amount_0={int(amount_0/ADDR_TO_DECIMAL_MULTIPLIER[token_0])}|amount_1={int(amount_1 / ADDR_TO_DECIMAL_MULTIPLIER[token_1])}|tick_lower={tick_lower}|tick_upper={tick_upper}')

        amt0, amt1 = int(amount_0 / ADDR_TO_DECIMAL_MULTIPLIER[token_0]), int(amount_1 / ADDR_TO_DECIMAL_MULTIPLIER[token_1])

        transaction = nft_manager.functions.mint(
            (
                token_0,
                token_1,
                fee,
                tick_lower,
                tick_upper,
                amt0,
                amt1,
                0,
                0,
                self.wallet_address,
                deadline,
            )
        ).build_transaction(
            {
                "nonce": self.current_nonce,
                "from": self.wallet_address,
                "type": "0x2",
                "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                + (2 * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]),
                "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
            }
        )
        self.current_nonce += 1
        transaction["gas"] = int(
            self.uniswap_instance.eth.estimate_gas(transaction) * 1.2
        )
        logging.info(transaction)
        signed_txn = self.uniswap_instance.eth.account.sign_transaction(
            transaction, private_key=self.acct.key
        )
        transaction = self.uniswap_instance.eth.send_raw_transaction(
            signed_txn.rawTransaction
        )
        receipt = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)
        return receipt

    def close_position(
        self,
        tokenId: int,
        amount0Min: int = 0,
        amount1Min: int = 0,
        deadline: int = None,
        *args,
        **kwargs,
    ) -> TxReceipt:
        """
        remove all liquidity from the position associated w/ tokenId, collect fees, and burn token.
        """
        if deadline is None:
            deadline = int(time.time()) + 2 * 60

        nft_manager = self.nonFungiblePositionManager
        try:
            (
                nonce,
                operator,
                token0,
                token1,
                fee,
                tick_lower,
                tick_upper,
                liquidity,
                feeGrowthInside0LastX128,
                feeGrowthInside1LastX128,
                tokensOwed0,
                tokensOwed1,
            ) = nft_manager.functions.positions(tokenId).call()
        except Exception as e:
            return None

        if token0 == WETH9_ADDRESS or token1 == WETH9_ADDRESS:
            amount0Min, amount1Min = nft_manager.functions.collect(
                (tokenId, _addr_to_str(self.wallet_address), MAX_UINT_128, MAX_UINT_128)
            ).call()

        # UNVERIFIED multicall routine to align with etherscan defacto standard for further improvement
        #
        # data = [nft_manager.encodeABI(
        #     fn_name = 'decreaseLiquidity',
        #     args = (tokenId, liquidity, amount0Min, amount1Min, deadline)
        # )] if liquidity > 0 else [] +
        # [nft_manager.encodeABI(
        #     fn_name = 'collect',
        #     args = (tokenId, _addr_to_str(self.wallet_address), MAX_UINT_128, MAX_UINT_128),
        # )] +
        # [nft_manager.encodeABI(
        #     fn_name = 'burn',
        #     args = tokenId,
        # )]
        # transaction = nft_manager.functions.multicall(
        #     data
        # ).build_transaction(
        #     {
        #         "nonce": self.current_nonce,
        #         "from": self.wallet_address,
        #         "type": "0x2",
        #         "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
        #         + (2 * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]),
        #         "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
        #     }
        # )
        # self.current_nonce += 1
        # transaction["gas"] = int(
        #     self.uniswap_instance.eth.estimate_gas(transaction) * 1.2
        # )
        # logging.info(transaction)
        # signed_txn = self.uniswap_instance.eth.account.sign_transaction(
        #     transaction, private_key=self.acct.key
        # )
        # transaction = self.uniswap_instance.eth.send_raw_transaction(
        #     signed_txn.rawTransaction
        # )
        # return self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)


        logging.info(f'CLOSE_POSITION : tokenId={tokenId}|liquidity={liquidity}')
        if liquidity > 0:
            transaction = nft_manager.functions.decreaseLiquidity(
                (tokenId, liquidity, amount0Min, amount1Min, deadline)
            ).build_transaction(
                {
                    "nonce": self.current_nonce,
                    "from": self.wallet_address,
                    "type": "0x2",
                    "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                    + (2 * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]),
                    "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
                }
            )
            self.current_nonce += 1
            transaction["gas"] = int(
                self.uniswap_instance.eth.estimate_gas(transaction) * 1.2
            )
            logging.info(transaction)
            signed_txn = self.uniswap_instance.eth.account.sign_transaction(
                transaction, private_key=self.acct.key
            )
            transaction = self.uniswap_instance.eth.send_raw_transaction(
                signed_txn.rawTransaction
            )
            self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)

        # TODO exception handling here, what should we do if there exists a break in _collect_fee and _burn_nft
        # no more mint requests should be send.
        self._collect_fee(tokenId)
        return self._burn_nft(tokenId)

    @deprecated(reason = 'not useful right now')
    def decrease_liquidity(
        self, tokenId, ratio, amount0Min=0, amount1Min=0, deadline=None
    ) -> TxReceipt:
        # self.approve_NonfungiblePositionManager()
        if deadline is None:
            deadline = int(time.time()) + 2 * 60
        nft_manager = self.nonFungiblePositionManager

        (
            nonce,
            operator,
            token0,
            token1,
            fee,
            tick_lower,
            tick_upper,
            liquidity,
            feeGrowthInside0LastX128,
            feeGrowthInside1LastX128,
            tokensOwed0,
            tokensOwed1,
        ) = nft_manager.functions.positions(tokenId).call()

        if token0 == WETH9_ADDRESS or token1 == WETH9_ADDRESS:
            amount0Min, amount1Min = nft_manager.functions.collect(
                (tokenId, _addr_to_str(self.wallet_address), MAX_UINT_128, MAX_UINT_128)
            ).call()

        newLiquidity = int(liquidity * ratio)
        transaction_token = nft_manager.functions.decreaseLiquidity(
            (tokenId, newLiquidity, amount0Min, amount1Min, deadline)
        ).transact(
            {
                "from": self.wallet_address,
                "type": "0x2",
                "gas": self.uniswap_instance.eth.estimate_gas({}),
                "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                + (2 * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]),
                "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
            }
        )
        _ = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction_token)
        return self._collect_fee(tokenId)

    @deprecated(reason = 'not useful right now')
    def increase_liquidity(
        self, tokenId, amountAdd_token_0, amountAdd_token_1, deadline=None
    ) -> TxReceipt:
        # self.approve_NonfungiblePositionManager()
        if deadline is None:
            deadline = int(time.time()) + 2 * 60

        nft_manager = self.nonFungiblePositionManager

        transaction_token = nft_manager.functions.increaseLiquidity(
            (tokenId, amountAdd_token_0, amountAdd_token_1, 0, 0, deadline)
        ).transact(
            {
                "from": self.wallet_address,
                "type": "0x2",
                "gas": self.uniswap_instance.eth.estimate_gas({}),
                "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                + (2 * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]),
                "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
            }
        )
        receipt = self.uniswap_instance.eth.wait_for_transaction_receipt(
            transaction_token
        )
        return receipt

    def update_nft_token_positions(self):
        nft_manager = self.nonFungiblePositionManager
        number_of_positions = nft_manager.functions.balanceOf(
            self.wallet_address
        ).call()

        self.nft_tokens = [
            nft_manager.functions.tokenOfOwnerByIndex(self.wallet_address, idx).call()
            for idx in range(number_of_positions)
        ]

        logging.info(self.nft_tokens)

        update_token_list = dict.fromkeys(self.pools.keys(), [])

        for tokenid in self.nft_tokens:
            try:
                (
                    nonce,
                    operator,
                    token0,
                    token1,
                    fee,
                    tick_lower,
                    tick_upper,
                    liquidity,
                    feeGrowthInside0LastX128,
                    feeGrowthInside1LastX128,
                    tokensOwed0,
                    tokensOwed1,
                ) = self.nonFungiblePositionManager.functions.positions(tokenid).call()
            except Exception as e:
                print(e)
                continue

            token_pool_addr = self._get_pool_addr(token0, token1, fee)

            if token_pool_addr in self.pools.keys():
                update_token_list[token_pool_addr] += [tokenid]

        for k in update_token_list.keys():
            self.pools[k]['nft_tokens'] = update_token_list[k]

        return self.nft_tokens.copy()

    def swap_out(
        self,
        input_token,
        output_token,
        fee,
        amount_out,
        amount_in_maximum=((1 << 256) - 1),
        sqrtPriceLimit=0,
        deadline=None,
    ):
        if deadline is None:
            deadline = int(time.time()) + 2 * 60

        args = (
            input_token,
            output_token,
            fee,
            self.wallet_address,
            deadline,
            int(amount_out / ADDR_TO_DECIMAL_MULTIPLIER[output_token]),
            int(amount_in_maximum / ADDR_TO_DECIMAL_MULTIPLIER[input_token]),
            sqrtPriceLimit,
        )

        print(f"swap_out : {args}")

        logging.debug(args)
        logging.debug(type(args))

        transaction = self.router.functions.exactOutputSingle(args).build_transaction(
            {
                "nonce": self.current_nonce,
                "from": self.wallet_address,
                "type": "0x2",
                "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                + (2 * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]),
                "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
            }
        )
        self.current_nonce += 1
        transaction["gas"] = int(
            self.uniswap_instance.eth.estimate_gas(transaction) * 1.2
        )
        logging.info(transaction)
        signed_txn = self.uniswap_instance.eth.account.sign_transaction(
            transaction, private_key=self.acct.key
        )
        transaction = self.uniswap_instance.eth.send_raw_transaction(
            signed_txn.rawTransaction
        )
        receipt = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)
        return receipt

    def swap_in(
        self,
        input_token,
        output_token,
        fee,
        amount_in,
        amount_out_minimum=0,
        sqrtPriceLimit=0,
        deadline=None,
    ):
        if deadline is None:
            deadline = int(time.time()) + 2 * 60

        args = (
            input_token,
            output_token,
            fee,
            self.wallet_address,
            deadline,
            int(amount_in / ADDR_TO_DECIMAL_MULTIPLIER[input_token]),
            int(amount_out_minimum / ADDR_TO_DECIMAL_MULTIPLIER[output_token]),
            sqrtPriceLimit,
        )

        print(f"swap_in : {args}")

        logging.debug(args)
        logging.debug(type(args))

        transaction = self.router.functions.exactInputSingle(args).build_transaction(
            {
                "nonce": self.current_nonce,
                "from": self.wallet_address,
                "type": "0x2",
                "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                + (2 * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]),
                "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
            }
        )
        self.current_nonce += 1
        transaction["gas"] = int(
            self.uniswap_instance.eth.estimate_gas(transaction) * 1.2
        )
        logging.info(transaction)
        signed_txn = self.uniswap_instance.eth.account.sign_transaction(
            transaction, private_key=self.acct.key
        )
        transaction = self.uniswap_instance.eth.send_raw_transaction(
            signed_txn.rawTransaction
        )
        receipt = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)

        return receipt

    def place_order_and_fill_or_fail(
        self,
        *,
        product: UniswapProduct,
        price: [float, str],
        quantity: [float, str],
        order_side: UniswapOrderSide,
        order_type: UniswapOrderType = UniswapOrderType.LIMIT,
        slippage: float = 0.1,
    ):
        price = float(price)
        quantity = float(quantity)
        logging.info(
            f"Uniswap V3 Swap | order_side : {order_side}, product : {product}, price : {price}, quantity : {quantity}"
        )
        token_1, token_0, fee = UniswapV3NativePrivateClient.convert_product_to_pool(
            product
        )

        if order_side == UniswapOrderSide.BUY:
            fut = self._executor.submit(self.swap_out,
                token_0,
                token_1,
                fee,
                quantity,
                int(quantity * (price * (1 + slippage))),
            )
            return fut

        if order_side == UniswapOrderSide.SELL:
            fut = self._executor.submit(self.swap_in,
                token_1,
                token_0,
                fee,
                quantity,
                int(quantity * (price * (1 - slippage))),
            )
            return fut

        raise NotImplementedError()

    def get_order(self, order_id: int):
        raise NotImplementedError()

    def cancel_order(self, order_id):
        raise NotImplementedError()

    def get_balances(self):
        logging.debug("Start get balance")
        req_timestamp = time.time()

        NFT_tokens = self.update_nft_token_positions()

        token_balance = {
            symbol: {"currency": symbol, "hold": 0, "volume": 0}
            for symbol in TOKEN_TO_ADDRESS['ethereum'].keys()
        }

        for tkn in NFT_tokens:
            (
                nonce,
                operator,
                token0,
                token1,
                fee,
                tick_lower,
                tick_upper,
                liquidity,
                feeGrowthInside0LastX128,
                feeGrowthInside1LastX128,
                tokensOwed0,
                tokensOwed1,
            ) = self.nonFungiblePositionManager.functions.positions(tkn).call()

            pool_addr = self._get_pool_addr(token0, token1, fee)
            x, y = self.calculate_amount(tick_lower, tick_upper, liquidity, pool_addr)

            symbol0 = self._coin_addr_to_symbol(token0)
            if symbol0 not in token_balance:
                continue

            symbol1 = self._coin_addr_to_symbol(token1)
            if symbol1 not in token_balance:
                continue

            logging.info('underlying: %s, %s' % (x * ADDR_TO_DECIMAL_MULTIPLIER[token0], y * ADDR_TO_DECIMAL_MULTIPLIER[token1]))
            fee0, fee1 = self.get_unclaimed_fee(tkn)

            token_balance[symbol0]["hold"] += (x + fee0) * ADDR_TO_DECIMAL_MULTIPLIER[token0]
            token_balance[symbol1]["hold"] += (y + fee1) * ADDR_TO_DECIMAL_MULTIPLIER[token1]

        res_timestamp = time.time()

        if 'ETH' in self.currencies or 'WETH' in self.currencies:
            self.currencies += ['WETH', 'ETH']

        if 'BTC' in self.currencies or 'WBTC' in self.currencies:
            self.currencies += ['WBTC', 'BTC']

        self.currencies = list(set(self.currencies))

        token_list = [TOKEN_TO_ADDRESS['ethereum'][symb] for symb in list(self.currencies)]

        address_coin_balances = self.addr_balance_checker.functions.balances(
            [self.wallet_address],
            token_list
        ).call()

        for token, balance in zip(list(self.currencies), address_coin_balances):
            token_balance[token]["volume"] = balance * TOKEN_TO_DECIMAL_MULTIPLIER.get(
                token, 1e-6
            )

        logging.debug(f"{token_balance.values()}")

        return RestUpdate(
            req_timestamp,
            res_timestamp,
            list(token_balance.values()),
            requests.codes.ok,
        )

    def get_unclaimed_fee(self, tokenId):
        results = self.nonFungiblePositionManager.functions.collect(
            (tokenId, _addr_to_str(self.wallet_address), MAX_UINT_128, MAX_UINT_128)).call()
        logging.info("Fee0: %s", results[0])
        logging.info("Fee1: %s", results[1])
        return results[0], results[1]

    def _collect_fee(self, tokenId):
        transaction = self.nonFungiblePositionManager.functions.collect(
            (tokenId, _addr_to_str(self.wallet_address), MAX_UINT_128, MAX_UINT_128)
        ).build_transaction(
            {
                "nonce": self.current_nonce,
                "from": self.wallet_address,
                "type": "0x2",
                "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                + (2 * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]),
                "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
            }
        )
        self.current_nonce += 1
        transaction["gas"] = int(
            self.uniswap_instance.eth.estimate_gas(transaction) * 1.2
        )
        signed_txn = self.uniswap_instance.eth.account.sign_transaction(
            transaction, private_key=self.acct.key
        )
        transaction = self.uniswap_instance.eth.send_raw_transaction(
            signed_txn.rawTransaction
        )
        receipt = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)

        return receipt

    def _burn_nft(self, tokenId):
        transaction = self.nonFungiblePositionManager.functions.burn(tokenId).build_transaction(
            {
                "nonce": self.current_nonce,
                "from": self.wallet_address,
                "type": "0x2",
                "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                + (2 * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]),
                "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
            }
        )
        self.current_nonce += 1
        transaction["gas"] = int(
            self.uniswap_instance.eth.estimate_gas(transaction) * 1.2
        )
        signed_txn = self.uniswap_instance.eth.account.sign_transaction(
            transaction, private_key=self.acct.key
        )
        transaction = self.uniswap_instance.eth.send_raw_transaction(
            signed_txn.rawTransaction
        )
        receipt = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)


    def _approve(self, tokenId, amount):
        # TODO
        transaction = tokenId.functions.approve(
            _addr_to_str(self.nonFungiblePositionManager.address), amount
        ).transact({"from": self.wallet_address})
        return transaction

    @functools.lru_cache()
    def _coin_addr_to_symbol(self, token_addr):
        token_erc20_contract = _load_contract(
            self.uniswap_instance, "erc20", token_addr
        )
        symb = token_erc20_contract.functions.symbol().call().upper()
        return symb

    @functools.lru_cache()
    def _get_pool_addr(self, token0, token1, fee):
        return self.factory_contract.functions.getPool(token0, token1, fee).call()

    @staticmethod
    def _check_pool_info(token_0: Address, token_1: Address, fee):
        assert isinstance(token_0, Address), token_0
        assert isinstance(token_1, Address), token_1
        assert token_0 != token_1, f"Token addresses cannot be the same, {token_0}"
        assert fee in list(
            TICK_SPACING.keys()
        ), "Uniswap V3 only supports three levels of fees: 0.05%, 0.3%, 1%"

    def calculate_amount(self, tick_lower, tick_upper, liquidity, pool_addr):
        curr_tick = (
            _load_contract(self.uniswap_instance, "pool", pool_addr)
            .functions.slot0()
            .call()[1]
        )
        return _calculate_amount(tick_lower, tick_upper, curr_tick, liquidity)

    @staticmethod
    def convert_product_to_pool(product: UniswapProduct):
        symbol_0, symbol_1 = product.base.native_currency, product.quote.native_currency

        return (
            TOKEN_TO_ADDRESS['ethereum']["WETH" if symbol_0 == "ETH" else symbol_0],
            TOKEN_TO_ADDRESS['ethereum']["WETH" if symbol_1 == "ETH" else symbol_1],
            product._fee_tier * BP_TO_POOL_FEE_MULTIPLIER,
        )

    def get_token_contracts(self):
        if self.pools is None:
            return []

        addrs = []
        for info in self.pools.values():
            addrs += [info["token_base"], info["token_quote"]]
        addrs = list(set(addrs))

        contracts = [
            _load_contract(self.uniswap_instance, "erc20", Web3.to_checksum_address(addr))
            for addr in addrs if Web3.to_checksum_address(addr) != Web3.to_checksum_address(TOKEN_TO_ADDRESS['ethereum']["USDT"])
        ]

        # revoke USDT approval before approve USDT 
        if Web3.to_checksum_address(TOKEN_TO_ADDRESS['ethereum']["USDT"]) in [Web3.to_checksum_address(addr) for addr in addrs]:
            contracts.append(
                _load_contract(self.uniswap_instance, "usdt", Web3.to_checksum_address(TOKEN_TO_ADDRESS['ethereum']["USDT"]))
            )
        return contracts

    @run_once
    def approve_NonfungiblePositionManager(self):
        logging.info("approve NFTManager")
        contracts = self.get_token_contracts()
        for instance in contracts:
            tx = instance.functions.approve(
                deployment_address["NonfungiblePositionManager"], MAX_UINT_256
            ).build_transaction(
                {
                    "nonce": self.current_nonce,
                    "from": self.wallet_address,
                    "type": "0x2",
                    "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                    + (
                        2
                        * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]
                    ),
                    "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
                }
            )
            self.current_nonce += 1
            tx["gas"] = int(self.uniswap_instance.eth.estimate_gas(tx) * 1.2)
            logging.info(tx)
            signed_txn = self.uniswap_instance.eth.account.sign_transaction(
                tx, private_key=self.acct.key
            )
            transaction = self.uniswap_instance.eth.send_raw_transaction(
                signed_txn.rawTransaction
            )
            recipt = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)
            logging.info(recipt)

    @run_once
    def approve_SwapRouter(self):
        logging.info("approve SwapRouter")
        contracts = self.get_token_contracts()
        for instance in contracts:
            tx = instance.functions.approve(
                deployment_address["SwapRouter"], MAX_UINT_256
            ).build_transaction(
                {
                    "nonce": self.current_nonce,
                    "from": self.wallet_address,
                    "type": "0x2",
                    "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                    + (
                        2
                        * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]
                    ),
                    "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
                }
            )
            self.current_nonce += 1
            tx["gas"] = int(self.uniswap_instance.eth.estimate_gas(tx) * 1.2)
            logging.info(tx)
            signed_txn = self.uniswap_instance.eth.account.sign_transaction(
                tx, private_key=self.acct.key
            )
            transaction = self.uniswap_instance.eth.send_raw_transaction(
                signed_txn.rawTransaction
            )
            recipt = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)
            logging.info(recipt)

    def disapprove_NonfungiblePositionManager(self):
        logging.info("disapprove NFTManager")
        contracts = self.get_token_contracts()
        for instance in contracts:
            tx = instance.functions.approve(
                deployment_address["NonfungiblePositionManager"], 0
            ).build_transaction(
                {
                    "nonce": self.current_nonce,
                    "from": self.wallet_address,
                    "type": "0x2",
                    "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                    + (
                        2
                        * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]
                    ),
                    "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
                }
            )
            self.current_nonce += 1
            tx["gas"] = int(self.uniswap_instance.eth.estimate_gas(tx) * 1.2)
            logging.info(tx)
            signed_txn = self.uniswap_instance.eth.account.sign_transaction(
                tx, private_key=self.acct.key
            )
            transaction = self.uniswap_instance.eth.send_raw_transaction(
                signed_txn.rawTransaction
            )
            recipt = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)
            logging.info(recipt)

    def disapprove_SwapRouter(self):
        logging.info("disapprove SwapRouter")
        contracts = self.get_token_contracts()
        for instance in contracts:
            tx = instance.functions.approve(
                deployment_address["SwapRouter"], 0
            ).build_transaction(
                {
                    "nonce": self.current_nonce,
                    "from": self.wallet_address,
                    "type": "0x2",
                    "maxFeePerGas": self.uniswap_instance.eth.max_priority_fee
                    + (
                        2
                        * self.uniswap_instance.eth.get_block("latest")["baseFeePerGas"]
                    ),
                    "maxPriorityFeePerGas": self.uniswap_instance.eth.max_priority_fee,
                }
            )
            self.current_nonce += 1
            tx["gas"] = int(self.uniswap_instance.eth.estimate_gas(tx) * 1.2)
            logging.info(tx)
            signed_txn = self.uniswap_instance.eth.account.sign_transaction(
                tx, private_key=self.acct.key
            )
            transaction = self.uniswap_instance.eth.send_raw_transaction(
                signed_txn.rawTransaction
            )
            recipt = self.uniswap_instance.eth.wait_for_transaction_receipt(transaction)
            logging.info(recipt)

    def wait_for_receipt(self, tx):
        return self.uniswap_instance.eth.wait_for_transaction_receipt(tx)

    def __del__(self):
        pass
        # self.disapprove_NonfungiblePositionManager()
        # self.disapprove_SwapRouter()
